
package SSF.OS.WWW;

/*
 * httpClient.java
 * @author Andy Ogielski ato@renesys.com
 * @version 0.1 November 28 1999
 * @version 0.2 November 4, 2000
 * @version 0.3 June 2, 2001
 */

import SSF.OS.*;
import SSF.OS.Socket.*;
import SSF.OS.TCP.test.*;
import SSF.Net.*;
import SSF.Net.Util.*;
import com.renesys.raceway.DML.*;
import SSF.Util.Random.*;
import cern.jet.random.*;
import java.util.*;

/** Master class for creating HTTP client-server sessions.
 *  Obtains DML configuration specifying available servers and
 *  session parameters, and kicks-off the client.
 *
 */
public class httpClient extends ProtocolSession {


  /************************* Class Variables *********************/

  /** option to print out debug information (from DML) */
  boolean debug = false;

  /** option to print out summary session report (from DML) */
  boolean show_report = false;

  /** earliest start time after init() from DML */
  long start_time = SSF.Net.Net.seconds(1.0);

  /** nominal size of httpRequest/Response headers from/to httpClient */
  int httpHdrSize = 1000;

  /** does HTTP use persistent TCP connections (HTTP1.1)
   *  or not (HTTP1.0)
   */
   boolean persistent = false;

  /** does HTTP pipeline requests, i.e. send requests before waiting for 
   *  completion of a previous request.
   */
   boolean pipeline = false;

  /** Host entity where this pseudo-protocol is installed */
  Host localHost;

  /** Host's IP address */
  int localIP;

  /** Host's NHI address */
  String localNHI;

  /** Host's Socket ProtocolSession */
  socketMaster sockms;

  /** inner class specifying a server */
  class serverData {
    public String nhi;
    public int ip;
    public int port;

    public serverData(String server_nhi, int server_port){
      nhi = server_nhi;
      ip = IP_s.StrToInt(localHost.global_nhi_to_ip(nhi));
      port = server_port;
    }
  }

  /** Name of the server list */
  public String serverList;

  /** Servers known to this host */
  Vector serverVector = new Vector(1,5);

  /** total number of sessions, successful and failed */
  int numSessions = 0;

  /** number of failed sessions */
  int numFailed = 0;

  /* session error codes */
    public static final int ERR_CONNECT = 1;
    public static final int ERR_SENDREQUEST = 2;
    public static final int ERR_RCVHEADER = 3;
    public static final int ERR_RCVDATA = 4;
    public static final int ERR_CLOSE = 5;

  /************** client behavior Statistical Model ****************/

  /** generator of session interarrival times (in seconds) */
  RandomDistribution interSessnTimeGen;

  /** sleep time (in ticks) between session arrivals */
  long sleepTime;

  /** generator of number of page requests per session */
  RandomDistribution numPagesGen;

  /** generator of inter-page-request times (in seconds) in session */
  RandomDistribution interPageTimeGen;

  /** generator of inter-object-request delays, for a page (in seconds) */
  RandomDistribution requestDelayGen;

  /** generator of random server selection */
  RandomDistribution serverGen;

  /** session aborted if server doesn't deliver data within this time
      (in seconds)
   */
  double patienceInterval = 1200;

  /************ inner class for a single HTTP sesion ***************/

  class httpSession extends ProtocolSession {
    httpClient owner;
    Continuation ownerCont;
    long startTime;
    int sessNo;

    socketAPI sd;
    String srvNHI;    
    int srvIP;
    int srvPort;

    byte nextReqType;
    httpResponse resp;
    long offTime;

    int numPages;
    int numPagesRemaining;       // decremented after receiving last object
    int numObjectsRemaining;     // decremented after receiving object
    int totalObjects = 0;
    int totalSize = 0;
    int rcvObjSize;
    boolean connClosed = false;

    int numErrors = 0;

    SSF.OS.Timer patienceTimer;

    public httpSession(serverData ssd) {
      owner = httpClient.this;
      sessNo = owner.numSessions;
      srvNHI = ssd.nhi;
      srvIP  = ssd.ip;
      srvPort = ssd.port;
      // how many pages we'll request from this server in this session
      numPages = Math.max(1, (owner.numPagesGen).nextInt());
      numPagesRemaining = numPages;
    }

    public void begin(Continuation caller) {
      ownerCont = caller;
      startTime = owner.localHost.now();
      if (debug)
        sessionDebug("sessn begin numPages: " + numPages);
      nextReqType = httpRequest.PAGE;
      openConnection();
    }

    void openConnection() {
      try { 
        sd = owner.sockms.socket(this, "tcp");
        sd.connect(srvIP, srvPort, new Continuation() {
          public void success() {
            if(debug)
              sessionDebug(0, "connect() OK");
            sendRequest();
          }
          public void failure(int errno) {
            if(debug)
              sessionDebug(errno, "connect() failure:");
            ++numErrors;
            finalReport();
          }
       });
      } catch (ProtocolException e) {
        System.err.println("httpSession: " + e);
      }
    }

    Object[] outobj;
    Object[] inobj;

    // used only to close a persistent connection
    void sendClose() {
      outobj = new Object[1];
      outobj[0] = new httpRequest(httpRequest.CLOSE); 
      sd.write(outobj, httpHdrSize, new Continuation() {
        public void success() {
          if (debug)
            sessionDebug(0,((httpRequest)outobj[0]).toString() + " OK");
          try {
            sd.close(new Continuation() {
              public void success() {
                if(debug)
                  sessionDebug(0,"persistent close() OK"); // RETURNS AFTER 2MSL
              }
              public void failure(int errno) {
                if(debug)
                  sessionDebug(errno,"persistent close() error:");
              }
            });
          } catch (ProtocolException e) {
            System.err.println(e);
          }
          finalReport();
        }
        public void failure(int errno) {
          if (debug)
            sessionDebug(errno,((httpRequest)outobj[0]).toString() + " error:");
          ++numErrors;
          finalReport();
        }
      });
    }

    void sendRequest() {
      if(!owner.persistent)
        nextReqType |= httpRequest.CLOSE;
      outobj = new Object[1];
      outobj[0] = new httpRequest(nextReqType); 
      sd.write(outobj, httpHdrSize, new Continuation() {
        public void success() {
          if (debug)
            sessionDebug(0,((httpRequest)outobj[0]).toString() + " OK");
          rcvResponse();
        }
        public void failure(int errno) {
          if (debug)
            sessionDebug(errno,((httpRequest)outobj[0]).toString() + " request failure:");
          ++numErrors;
          finalReport();
        }
      });
    }

    void rcvResponse() {
      // need a timer if read never receives the data due to
      // server write failure
      patienceTimer = new SSF.OS.Timer(owner.localHost,
                                       SSF.Net.Net.seconds(patienceInterval)) {
        public void callback() {
          if (debug)
            sessionDebug(0,"time "+patienceInterval+
                                 " sec waiting for server exceeded");
            ++numErrors;
            finalReport();
            cancel();
        }
      };

      patienceTimer.set(SSF.Net.Net.seconds(patienceInterval));

      inobj = new Object[1];
      sd.read(inobj, owner.httpHdrSize, new Continuation() {
        public void success() {
          resp = (httpResponse)inobj[0];
          if (debug)
            sessionDebug(0,resp.toString() + " OK");

          if((resp.responseType & httpRequest.PAGE) == httpRequest.PAGE) {
            numObjectsRemaining = resp.numObjects;
          }

          sd.read(resp.objSize, new Continuation() {
            public void success() {
              if (debug)
                sessionDebug(0,"read() " + resp.objSize + "B OK");
              if(--numObjectsRemaining == 0)  // read last object in page
                --numPagesRemaining;
              ++totalObjects;
              totalSize += resp.objSize;
              patienceTimer.cancel();
              continueSession();
            }
            public void failure(int errno) {
              if (debug)
                sessionDebug(errno,((httpRequest)outobj[0]).toString() + " rcvResponse failure:");
              ++numErrors;
              patienceTimer.cancel();
              finalReport();
            }
          });
        }
        public void failure(int errno) {
          ++numErrors;
          patienceTimer.cancel();
          finalReport();
        }
      });
    }

    // if non-persistent then close connection before continuing

    void continueSession() {
      if(owner.persistent) {
        if(numPagesRemaining == 0)   // persistent session completed
          sendClose();
        else
          setupNextRequest();
      } else {
        try {
          sd.close(new Continuation() {
            public void success() {
              if(debug)
                sessionDebug(0,"non-persistent close() OK");
            }
            public void failure(int errno) {
              if(debug)
                sessionDebug(errno,"non-persistent close() error:");
            }
          });
        } catch (ProtocolException e) {
          System.err.println(e);
        }
        if(numPagesRemaining == 0)  // non-persistent session completed
          finalReport();
        else
          setupNextRequest();
      }
    }

    // current value of numObjectsRemaining is available,
    // numPagesRemaining = 0 never enters this method

    void setupNextRequest() {
      double dummy;

      if(numObjectsRemaining > 0) {  
        nextReqType = httpRequest.OBJECT;
        //offTimer(SSF.Net.Net.seconds(requestDelayGen.nextDouble()));
          dummy =requestDelayGen.nextDouble();
            if(debug)
              sessionDebug("requestDelay: " + dummy);
          offTimer(SSF.Net.Net.seconds(dummy));
      } else {
        nextReqType = httpRequest.PAGE;
        //offTimer(SSF.Net.Net.seconds(interPageTimeGen.nextDouble()));
          dummy =interPageTimeGen.nextDouble();
            if(debug)
              sessionDebug("interPageInterval: " + dummy);
          offTimer(SSF.Net.Net.seconds(dummy));
          //offTimer(SSF.Net.Net.seconds(interPageTimeGen.nextDouble()));
      }
    }

    void offTimer(long delay) {
      (new SSF.OS.Timer(owner.localHost, delay){
        public void callback() {
          if(owner.persistent)
            sendRequest();
          else {
            openConnection();      // it's Continuation will do sendRequest()
          }
          cancel();
        }
      }).set(delay);
    }

    void finalReport() {
      String str = "";
      if(show_report || debug) {
        double duration = (localHost.now() -
                          startTime)/(double)SSF.Net.Net.seconds(1.0);
        str = "#pages: " + numPages
                     + " #objects: " + totalObjects
                     + " total: " + totalSize
                     + "B seconds: " + duration;
        }
      if(numErrors > 0) {
        if(show_report ||  debug)
          sessionReport(str + " #errors: " + numErrors + " FAILURE");
        ownerCont.failure(-1);
      } else {
        if(show_report || debug)
          sessionReport(str + " SUCCESS");
        ownerCont.success();
      }
    }


    /** preamble to client-side diagnostics */
    public void sessionDebug(int errno, String str){

      StringBuffer lineBuffer = new StringBuffer(200);
      lineBuffer.append(localHost.now()/(double)SSF.Net.Net.seconds(1.0))
        .append(" "+owner.name+" ").append(localNHI).append(" ")
        .append(IP_s.IPtoString(localIP)).append(" sid ").append(sessNo)
        .append(" "+str);

      if(errno>0)
        lineBuffer.append(" "+socketMaster.errorString(errno));
      System.err.println(lineBuffer);
    }

    /* preamble to client-side-only diagnostics */
    public void sessionDebug(String str){
      System.err.println(owner.localHost.now()/(double)SSF.Net.Net.seconds(1.0)
                         + " clnt " + owner.localNHI 
                         + " " + IP_s.IPtoString(owner.localIP)
                         + " sid " + sessNo + " " + str);
    }

    /* preamble to end2end session diagnostics */
    public void sessionReport(String str){
      System.err.println(owner.localHost.now()/(double)SSF.Net.Net.seconds(1.0)
              + " [ sid " + sessNo + " start " 
              + startTime/(double)SSF.Net.Net.seconds(1.0) 
              + " ] clnt " + owner.localNHI
              + " srv " + srvNHI + " " + str);
    }

    public boolean push(ProtocolMessage message, ProtocolSession fromSession)
      throws ProtocolException {
      return false;
    }
  } // end of httpSession

  /*************************** Constructors ************************/

  public httpClient () {}

  /************************* Client Operation ***********************/

  /** preamble to client-side-only diagnostics */
  public void clientDebug(String str){
    System.err.println("\n"+localHost.now()/(double)SSF.Net.Net.seconds(1.0)
                       + " clnt " + localNHI
                       + " " + IP_s.IPtoString(localIP)
                       + " " + str);
  }

  /** preamble to client-side diagnostics */
  public void clientDebug(int errno, String str){
    //if (!debug) return;

    StringBuffer lineBuffer = new StringBuffer(200);
    lineBuffer.append(localHost.now()/(double)SSF.Net.Net.seconds(1.0))
      .append(" "+this.name+" ").append(localNHI).append(" ")
      .append(IP_s.IPtoString(localIP)).append(" ").append(str);

    if(errno>0)
      lineBuffer.append(" "+socketMaster.errorString(errno));
    System.err.println(lineBuffer);
  }

  /** Kick-off the sessionArrivalProcess mode. */
  public void init() throws ProtocolException {
    IP ipsess;

    try {
      sockms = (socketMaster)inGraph().SessionForName("socket");
    } catch (ProtocolException e) { 
      System.err.println(e);
    }

    try {
      ipsess = (IP)localHost.SessionForName("ip");
      localIP = (int)((NIC)ipsess.INTERFACES.elementAt(0)).ipAddr;
    } catch (ProtocolException e) { 
      System.err.println(e);
    }

    // delay the earliest start time of the first session
    start_time += SSF.Net.Net.seconds(interSessnTimeGen.nextDouble());

    (new SSF.OS.Timer(localHost, start_time){
      public void callback() {
        sessionArrivalProcess();
        cancel();
      }
    }).set(start_time);
  }

  /** Session renewal process:  sleep for a random time distributed
   *  according to interSessnTimeGen, then open a session to a randomly
   *  selected server,  then after the session is completed either
   *  with success or failure, repeat the off-on cycle. 
   */
  public void sessionArrivalProcess() {
    serverData srv;
    httpSession sess;

    if(serverVector.isEmpty())
      return;

    // choose a server randomly
    int index = ((Uniform)serverGen.getImplementation()).nextIntFromTo(0,serverVector.size()-1);
    srv = (serverData)serverVector.elementAt(index);
    sess = new httpSession(srv);

    // expand for debugging
    //sleepTime = SSF.Net.Net.seconds(interSessnTimeGen.nextDouble());
    double dummy;
    dummy =interSessnTimeGen.nextDouble();
    if(debug)
      clientDebug("interSessnTime: " + dummy);
    sleepTime = SSF.Net.Net.seconds(dummy);

    sess.begin(new Continuation() {
      public void success() {
        ++numSessions;
        // start a timer for the next session
        (new SSF.OS.Timer(localHost, sleepTime){
          public void callback() {
            sessionArrivalProcess();
            cancel();
          }
        }).set(sleepTime);
      }
      public void failure(int errno) {
        ++numFailed;
        // start a timer for the next session
        (new SSF.OS.Timer(localHost, sleepTime){
          public void callback() {
            sessionArrivalProcess();
            cancel();
          }
        }).set(sleepTime);
      }
    });
  }

  /************************* Configuration ***********************/

  /** Configure the client. Example of the supported DML attributes:
   * <PRE>
   *   ProtocolSession [ name client use SSF.OS.WWW.httpClient
   *
   *     # earliest time to send the first request to server (seconds).
   *
   *     start_time 1.0
   *
   *     # HTTP session arrival process in this client. The first session
   *     # begins at start_time + inter_session_time (in seconds).
   *     # Each session is with a server chosen uniformly at random from
   *     # the servers specified in the .Net.traffic for this client.
   *
   *     inter_session_time [
   *       distribution [
   *         name "Exponential"         # distribution class name
   *         lambda 0.01                # mean = 1/lambda
   *       ]
   *     ]
   *
   *     # distribution of number of pages per session with selected server.
   *     # if random variable = 0, round up to 1
   *
   *     pages_in_session [
   *       distribution [
   *         name "Exponential"         # distribution class name
   *         lambda 0.2                 # mean = 1/lambda
   *       ]
   *     ]
   *
   *     # distribution of user-think-times (off-times) between consecutive
   *     # in-session page requests, counted from the time when the previous
   *     # page delivery is completed (in seconds).
   *
   *     inter_page_time [
   *       distribution [
   *         name "Pareto"              # distribution class name
   *         k 25.0                     # scale (cutoff) parameter
   *         alpha 2.0                  # shape (exponent) parameter
   *       ]
   *     ]
   *
   *     # distribution of delays between consecutive requests in a session.
   *     # Counted from the time when the previous request was completed. (In seconds.)
   *
   *     inter_request_time [
   *       distribution [
   *         name "Pareto"              # distribution class name
   *         k 0.1667                   # scale (cutoff) parameter
   *         alpha 1.5                  # shape (exponent) parameter
   *       ]
   *     ]
   *
   *     http_hdr_size 1000  # nominal HTTP header size (virtual bytes) read from a socket
   *                         # before reading data (if indicated by http header)
   *     persistent false    # if true, one persistent TCP connection per session (HTTP1.1),
   *                         # if false,one TCP connection for each request/response (HTTP1.0).
   *     show_report true    # print client-server session summary report
   *     debug false         # print verbose client/server diagnostics
   *
   * # new atributes in ssfnet_1.3 - backward compatible with ssfnet_1.2
   *  ------------------------------------------------------------------
   *
   *    server_list  "mylist" # identifies server list(s) name in traffic pattern
   *                          # if omitted concatenate all server lists in
   *                          # matching pattern
   *   ]
   * </PRE>
  */
  public void config(Configuration cfg) throws configException {
    super.config(cfg);
    localHost = (SSF.Net.Host)inGraph();
    localNHI = localHost.nhi;
    String strTarget;

    if((strTarget = (String)cfg.findSingle("start_time")) != null)
      start_time = SSF.Net.Net.seconds((new Float(strTarget)).floatValue());
    else
      throw new configException ("start_time not found");

    if((strTarget = (String)cfg.findSingle("http_hdr_size")) != null)
      httpHdrSize = (new Integer(strTarget)).intValue();
    else
      throw new configException ("http_hdr_size not found");

    if((strTarget = (String)cfg.findSingle("persistent")) != null)
      if(strTarget.compareTo("true") == 0)
        persistent = true;

    if((strTarget = (String)cfg.findSingle("pipeline_requests")) != null)
      if(strTarget.compareTo("true") == 0)
        pipeline = true;

    if((strTarget = (String)cfg.findSingle("show_report")) != null)
      if(strTarget.compareTo("true") == 0)
        show_report = true;

    if((strTarget = (String)cfg.findSingle("debug")) != null)
      if(strTarget.compareTo("true") == 0)
        debug = true;

  /****************** Client Statistical Model ******************/

    Configuration scfg = (Configuration)cfg.findSingle("inter_session_time");
    if(scfg == null)
      throw new configException
        ("inter_session_time not found");
    scfg = (Configuration)scfg.findSingle("distribution");
    if(scfg == null)
      throw new configException
        ("inter_session_time.distribution not found");

    interSessnTimeGen = Net.accessRandomDistribution(this, scfg, "inter_session_time"); 

    scfg = (Configuration)cfg.findSingle("pages_in_session");
    if(scfg == null)
      throw new configException
        ("pages_in_session not found");
    scfg = (Configuration)scfg.findSingle("distribution");
    if(scfg == null)
      throw new configException
        ("pages_in_session.distribution not found");

    numPagesGen = Net.accessRandomDistribution(this, scfg, "pages_in_session");

    scfg = (Configuration)cfg.findSingle("inter_page_time");
    if(scfg == null)
      throw new configException
        ("inter_page_time not found");
    scfg = (Configuration)scfg.findSingle("distribution");
    if(scfg == null)
      throw new configException
        ("inter_page_time.distribution not found");

    interPageTimeGen = Net.accessRandomDistribution(this, scfg, "inter_page_time");

    scfg = (Configuration)cfg.findSingle("inter_request_time");
    if(scfg == null)
      throw new configException
        ("inter_request_time not found");
    scfg = (Configuration)scfg.findSingle("distribution");
    if(scfg == null)
      throw new configException
        ("inter_request_time.distribution not found");

    requestDelayGen = Net.accessRandomDistribution(this, scfg, "inter_request_time");

    serverGen = Net.accessRandomDistribution(this, "Uniform", "serverChoice");

  /****************** Client-Server Traffic Model ***************/

    if((strTarget = (String)cfg.findSingle("server_list")) != null) {
      serverList = strTarget;
      if(debug)
        clientDebug("config: serverList= "+serverList);
    }

    Configuration traffic = (Configuration)cfg.findSingle(".Net.traffic");
    if(traffic == null)
      throw new configException
        (".Net.traffic not found");
    configTraffic(traffic, localNHI, serverVector);
  }

  /** Add a list of servers available to the client(s) to the server_list.
   *  A client may match multiple NHI prefixes; the servers 
   *  specified by multiple matches are simply added to the server_list
   *  without checking for duplication.
   *  All NHI addresses must be global.
   *  A given client_nhi matches the value of the attribute "client foo'
   *  iff the NHI string "client_nhi" extends the NHI value "foo", viz. 
   *  client_nhi = foo:* or client_nhi = foo.
   *
   *  The attribute traffic can have the forms
   *<pre>
   *  traffic [
   *    pattern [
   *      client 1:1        # value N or N:H allowed, where N is NHI network prefix
   *      servers [list "caseA" nhi 1:2(0) port 10]  # single server
   *    ]
   *    pattern [
   *      client 2:1
   *      servers [list "caseB" nhi_range [from 2:2(0) to 2:5(0)] port 11] # vector of servers
   *    ]
   *  ]
   *</pre>
   *
   *  In case of a range of nhi addreses, an allowed range is of the form
   *  from N:h1(i) to N:h2(i), where N is a valid Net prefix, 
   *  and h1 <= h2 is a range of hosts.<P>
   *
   *  The optional attribute <CODE>servers.list</CODE> introduced in ssfnet_1.3
   *  can be matched by tcpClient or httpClient attribute <CODE>server_list</CODE>
   *  to narrow down a list of servers, and - more importantly - to allow
   *  a client application to distinguish multiple servers on a single host (Note
   *  that tcpClient and httpClient configuration does not specify the server port
   *  number; instead a client searches the list of all servers in the
   *  <CODE>.Net.traffic</CODE> attribute, and chooses the ones that match the client's
   *  host NHI address AND also match the <CODE>server_list</CODE> attribute value
   *  string.
   */
  public void configTraffic(Configuration traffic, String client_nhi, Vector server_list)
         throws configException {
    String str;
    Configuration srv;
    int port;
    if(traffic == null) return;

    for (Enumeration patterns = traffic.find("pattern");
         patterns.hasMoreElements();) {

      Configuration pattern = (Configuration)patterns.nextElement();
      str = (String)pattern.findSingle("client");
      if(!nhi_matches(str, client_nhi))
        continue;

      for (Enumeration servers = pattern.find("servers"); servers.hasMoreElements();) {
        srv = (Configuration)servers.nextElement();

        if(serverList != null) {
          str = (String)srv.findSingle("list");
          if((str == null) || !(serverList.compareTo(str) == 0))
            continue;
        }

        if((str = (String)srv.findSingle("port")) == null)
          throw new configException
            ("traffic.pattern.servers.port missing: " + servers);
        port = (new Integer(str)).intValue();

        // case of a single nhi server address
        if((str = (String)srv.findSingle("nhi")) !=null) {
          server_list.addElement(new serverData(str, port));
          if(debug)
            clientDebug("configTraffic: adding server "+str+" port "+port);
          continue;
        }

        // case of a range of nhi addreses. An allowed range is of the form
        // from N:h1(i) to N:h2(i), where N is a valid Net prefix, 
        // and h1 <= h2 is a range of hosts.
        Configuration nhi = (Configuration)srv.findSingle("nhi_range");
        if(nhi == null)
          throw new configException
            ("traffic.pattern.servers must have either nhi or nhi_range: " + servers);

        String nhi_min = (String)nhi.findSingle("from");
        String nhi_max = (String)nhi.findSingle("to");
        String net_min = nhi_min.substring(0,nhi_min.lastIndexOf(":")+1);
        String net_max = nhi_max.substring(0,nhi_max.lastIndexOf(":")+1);
        String host_min = nhi_min.substring(nhi_min.lastIndexOf(":")+1,
                          nhi_min.indexOf("("));
        String host_max = nhi_max.substring(nhi_max.lastIndexOf(":")+1,
                          nhi_max.indexOf("("));
        String iface_min = nhi_min.substring(nhi_min.indexOf("("),
                           nhi_min.indexOf(")")+1);
        String iface_max = nhi_max.substring(nhi_max.indexOf("("),
                           nhi_max.indexOf(")")+1);

        if (!net_min.equals(net_max))
          throw new configException
            ("traffic.pattern.servers.nhi_range: all network prefixes must be equal");

        if (!iface_min.equals(iface_max))
          throw new configException
            ("traffic.pattern.servers.nhi_range: all interface IDs must be equal");

        int hid_min = (new Integer(host_min)).intValue();
        int hid_max = (new Integer(host_max)).intValue();
        if (hid_min>hid_max)
          throw new configException
            ("traffic.pattern.servers.nhi_range: Host IDs must be ascending");

        for (int hid = hid_min; hid<=hid_max; hid++) {
          String host_nhi = net_min+hid+iface_min;
          serverVector.addElement(new serverData(host_nhi, port));
          if(debug)
            clientDebug("configTraffic: adding server "+host_nhi+" port "+port);
        }
      }
    }
    if(debug)
      clientDebug("configTraffic: added "+serverVector.size()+" servers");
  }

  /** Determine whether the NHI address 'prefix' matches the NHI address 'candidate'. 
   *  This is true iff the NHI address 'candidate' extends the NHI address 
   *  'prefix': candidate = prefix:* or candidate = prefix.
   */
  private boolean nhi_matches(String prefix, String candidate) {
    if (!candidate.startsWith(prefix)) return false;
    if (candidate.length()>prefix.length() && 
        candidate.charAt(prefix.length()) != ':')
      return false;
    return true;
  }

  public boolean push(ProtocolMessage message, ProtocolSession fromSession)
    throws ProtocolException {
    return false;
  }
}
